package guiservice.server.logic;

        import guiservice.common.controls.*;
        import guiservice.common.controls.handlers.KeyEventArgs;
        import guiservice.common.controls.handlers.MouseEventArgs;
        import guiservice.common.controls.handlers.MouseEventType;
        import guiservice.common.net.Client;
        import lombok.Setter;
        import lombok.extern.log4j.Log4j;

        import java.awt.Graphics;
        import java.util.*;
        import java.util.concurrent.ConcurrentHashMap;
        import java.util.concurrent.locks.Lock;
        import java.util.concurrent.locks.ReentrantLock;
        import javax.swing.JPanel;

@Log4j
public class ControlsManager {

    private static ControlsManager instance;

    //Примитив синхронизации
    private Lock lock;
    private final ConcurrentHashMap<java.lang.Long, Client> mapIdToClient = new ConcurrentHashMap<java.lang.Long, Client>();

    public static ControlsManager getInstance() {
        if (instance == null) {
            instance = new ControlsManager();
        }
        return instance;
    }

    @Setter
    private JPanel board;

    private ControlsManager() {
        mapOwnerToChildren = new Hashtable();
        mapIdToControl = new Hashtable();
        mapWindowToOrder = new Hashtable();
        mapControlToClient = new Hashtable();
        mapWindowToFocusedControl = new Hashtable();
        lock = new ReentrantLock();
    }

    void updateUI() {
        if (board != null) {
            board.updateUI();
        }
    }

    public List<java.lang.Long> unregisterClient(Client client) {
        java.lang.Long uid = -1L;
        for (Map.Entry<java.lang.Long, Client> entry : mapIdToClient.entrySet()) {
            if (entry.getValue().equals(client)) {
                uid = entry.getKey();
                log.info(String.format("Disconnect client with id: %d", uid));
                break;
            }
        }
        List<java.lang.Long> result = new ArrayList<>();
        if (uid != -1) {
            for (Map.Entry<java.lang.Long, Control> pair : mapIdToControl.entrySet()) {
                if (mapControlToClient.get(pair.getValue()) == client) {
                    result.add(pair.getKey());
                }
            }
            for (java.lang.Long id : result) {
                mapOwnerToChildren.remove(mapIdToControl.get(id));
                mapControlToClient.remove(mapIdToControl.get(id));
                Control remove = mapIdToControl.remove(id);
                if (remove != null && remove instanceof guiservice.server.controls.Window) {
                    mapWindowToFocusedControl.remove(remove);
                    mapWindowToOrder.remove(remove);
                }
            }
            mapIdToClient.remove(uid);
        }
        return result;
    }

    public void registerClient(Client client, java.lang.Long uid) {
        mapIdToClient.put(uid, client);
    }

    private Hashtable<Window, Integer> mapWindowToOrder;

    private Hashtable<Control, List<Control>> mapOwnerToChildren;

    private Hashtable<java.lang.Long, Control> mapIdToControl;

    private Hashtable<Control, Client> mapControlToClient;

    private Hashtable<Window, Control> mapWindowToFocusedControl;

    private Window clickedWindow;

    private int prevMovedX = -1;
    private int prevMovedY = -1;

    public <T> T getControl(long id, Class<T> cls) {
        Control control = mapIdToControl.get(id);
        if (control != null) {
            return (T) control;
        }
        return null;
    }

    public Control registerControl(Control control, java.lang.Long uid, long uidOfOwnerClient) {
        lock.lock();
        mapControlToClient.put(control, mapIdToClient.get(uidOfOwnerClient));
        mapIdToControl.put(uid, control);

        if (control.getClass() == guiservice.server.controls.Window.class) {
            //board.add(new JFrame());
            mapOwnerToChildren.put(control, new ArrayList());
            incrementOrder();
            mapWindowToOrder.put((Window) control, 0);
        } else if (control.isContainer()){
            mapOwnerToChildren.get(mapIdToControl.get(control.getOwner())).add(control);
            mapOwnerToChildren.put(control, new ArrayList());
        } else {
            mapOwnerToChildren.get(mapIdToControl.get(control.getOwner())).add(control);
        }
        lock.unlock();
        updateUI();
        return control;
    }

    private Window[] getOrderedWindows() {
        Window[] windows = new Window[mapWindowToOrder.size()];
        windows = mapWindowToOrder.keySet().toArray(windows);
        Arrays.sort(windows, new Comparator<Window>() {

            @Override
            public int compare(Window o1, Window o2) {
                if (mapWindowToOrder.get(o1) < mapWindowToOrder.get(o2)) {
                    return -1;
                }
                return 1;
            }

        });
        return windows;
    }

    public void redraw(Graphics g) {
        if (board == null) {
            return;
        }
        lock.lock();
        try {
            log.info("Redraw");
            Window[] windows = getOrderedWindows();
            for (int i = windows.length - 1; i > -1; i--) {
                Window window = windows[i];
                Graphics windowGraphics = window.getGraphics();
                if (windowGraphics != null) {
                    redrawChildContainers(windowGraphics, window);
                    window.draw(g, board);
                }
            }
        } catch (Exception e) {
            log.error("Error occurred while redrawing gui ", e);
        }
        lock.unlock();
    }

    private void redrawChildContainers(Graphics g, Control control) {
        if (board == null) {
            return;
        }
        //Отрисовываем сначала все контейнеры
        for (Control childContainer : mapOwnerToChildren.get(control)) {
            if (childContainer.isContainer()) {
                Graphics windowGraphics = childContainer.getGraphics();
                //отрисовываем дочерние контейнеры
                redrawChildContainers(g, childContainer);
                childContainer.draw(g, board);
            }
        }
        //потом обычные дочерние элементы
        for (Control child : mapOwnerToChildren.get(control)) {
            if (!child.isContainer()) {
                child.draw(g, board);
            }
        }
    }

    public void onMouseDownHandler(int x, int y) {
        clickedWindow = trySendMouseEvent(x, y, MouseEventType.Pressed, 0);
        if (clickedWindow != null) {
            log.debug("Clicked window id = " + clickedWindow.getId());
        }
        prevMovedX = x;
        prevMovedY = y;
    }

    public void onMouseUpHandler(int x, int y) {
        if (clickedWindow != null) {
            mapControlToClient.get(clickedWindow).eventHandler(clickedWindow.getId(), new MouseEventArgs(x, y, MouseEventType.Released, 0));
            int diffX = x - prevMovedX;
            int diffY = y - prevMovedY;
            clickedWindow.setPoint(clickedWindow.getX() + diffX, clickedWindow.getY() + diffY);
            updateUI();
        } else {
            trySendMouseEvent(x, y, MouseEventType.Released, 0);
        }
        clickedWindow = null;
        prevMovedX = x;
        prevMovedY = y;

    }

    public void onMouseMovedHandler(int x, int y) {
        if (clickedWindow != null) {
            mapControlToClient.get(clickedWindow).eventHandler(clickedWindow.getId(), new MouseEventArgs(x, y, MouseEventType.Moved, 0));
            if (prevMovedX == -1 || prevMovedY == -1) {
                prevMovedX = x;
                prevMovedY = y;
            } else {
                int diffX = x - prevMovedX;
                int diffY = y - prevMovedY;
                prevMovedX = x;
                prevMovedY = y;
                clickedWindow.setPoint(clickedWindow.getX() + diffX, clickedWindow.getY() + diffY);
                updateUI();
            }
        }
    }

    public void onMouseClickedHandler(int x, int y) {
        trySendMouseEvent(x, y, MouseEventType.Clicked, 0);
        clickedWindow = null;
        prevMovedX = -1;
        prevMovedY = -1;
    }

    private Window trySendMouseEvent(int x, int y, MouseEventType type, int scrollAmount) {
        try {
            Window clickedWindow = getWindow(x, y);
            Control selectedControl = getControl(x, y);
            if (selectedControl != null) {
                MouseEventArgs mouseEventArgs = new MouseEventArgs(x, y, type, scrollAmount);
                long id = selectedControl.getId();
                log.debug(String.format("id=%d, %s", id, mouseEventArgs));
                mapControlToClient.get(selectedControl).eventHandler(id, mouseEventArgs);
            }
            if (clickedWindow != null) {
                mapWindowToFocusedControl.put(clickedWindow, selectedControl);
            }
            return clickedWindow;
        } catch (Exception e) {
            log.error("Error occurred while processing key event", e);
            return null;
        }
    }

    private Window getWindow(int x, int y) {
        for (Window window : getOrderedWindows()) {
            if (window.contains(x, y)) {
                if (mapWindowToOrder.get(window) != 0) {
                    mapWindowToOrder.remove(window);
                    incrementOrder();
                    mapWindowToOrder.put(window, 0);
                }
                return window;
            }
        }
        return null;
    }

    private Control getControl(int x, int y) {
        Window topWindow = null;
        for (Window window : getOrderedWindows()) {
            if (window.contains(x, y)) {
                if (mapWindowToOrder.get(window) != 0) {
                    topWindow = window;
                }
                Control result = getControlInContainer(window,
                        x - (window.getX() + window.getXOffset()),
                        y - (window.getY() + window.getYOffset()));
                if (result == null) {
                    return window;
                } else {
                    return result;
                }
            }
        }
        if (topWindow != null) {
            mapWindowToOrder.remove(topWindow);
            incrementOrder();
            mapWindowToOrder.put(topWindow, 0);
        }
        return null;
    }

    private Window getFocusedWindow() {
        Window[] windows = getOrderedWindows();
        if (windows.length != 0) {
            return windows[0];
        }
        return null;
    }

    private Control getControlInContainer(Control control, int x, int y) {
        Control result = null;
        for (Control childContainer : mapOwnerToChildren.get(control)) {
            if (childContainer.isContainer() && childContainer.contains(x, y)) {
                result = getControlInContainer(childContainer,
                        x - childContainer.getX(), y - childContainer.getY());
                break;
            }
        }
        if (result != null) {
            return result;
        }
        for (Control child : mapOwnerToChildren.get(control)) {
            if (!child.isContainer() && child.contains(x, y)) {
                result = child;
                break;
            }
        }
        return result;
    }

    private void incrementOrder() {
        for (Window window : mapWindowToOrder.keySet()) {
            mapWindowToOrder.put(window, mapWindowToOrder.get(window) + 1);
        }
    }

    public void onKeyPressed(String keyText, String keyModifiersText) {
        keyHandle(keyText, keyModifiersText, true);
    }

    public void onKeyReleased(String keyText, String keyModifiersText) {
        keyHandle(keyText, keyModifiersText, false);
    }

    private void keyHandle(String keyText, String keyModifiersText, boolean isPressed) {
        try {
            Window window = getFocusedWindow();
            if (window != null && mapWindowToFocusedControl.containsKey(window)) {
                Control control = mapWindowToFocusedControl.get(window);
                if (control != null) {
                    KeyEventArgs keyEventArgs = new KeyEventArgs(keyText, keyModifiersText, isPressed);
                    long id = control.getId();
                    log.debug(String.format("id=%d, %s", id, keyEventArgs));
                    mapControlToClient.get(control).eventHandler(id, keyEventArgs);
                }
            }
        } catch (Exception e) {
            log.error("Error occurred while processing key event", e);
        }
    }

    public void unregisterControl(java.lang.Long id) {
        Control control = mapIdToControl.get(id);
        if (control instanceof guiservice.server.controls.Window) {
            mapWindowToFocusedControl.remove(control);
            mapWindowToOrder.remove(control);
            if (clickedWindow.getId() == id) {
                clickedWindow = null;
            }
        }
        mapControlToClient.remove(control);
        mapOwnerToChildren.remove(control);
        mapIdToControl.remove(id);
    }

    public void onScrollHandler(int x, int y, int scrollAmount) {
        trySendMouseEvent(x,y, MouseEventType.Scroll, scrollAmount);
    }

    public java.lang.Long[] getClients() {
        return mapIdToClient.keySet().toArray(new java.lang.Long[0]);
    }

    public Long[] getControls(long uid, String cls) {
        Client client = mapIdToClient.get(uid);
        List<Long> result = new ArrayList<>();
        for (Map.Entry<Control, Client> entry : mapControlToClient.entrySet()) {
            log.debug(String.format("Type=%s, id=%d", entry.getKey().getControlType(), entry.getKey().getId()));
            if (entry.getValue() == client && entry.getKey().getClass().getCanonicalName().equals(cls)) {
                result.add(entry.getKey().getId());
            }
        }
        return result.toArray(new Long[0]);
    }

    public Client getClientById(long uid) {
        return mapIdToClient.get(uid);
    }
}
